﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization;
using System.Xml;
using System.Xml.Linq;
using System.Text.RegularExpressions;
using System.Diagnostics;
using Pws.Clients.RestLibrary.Transport;
using Pws.Clients.RestLibrary.Service;
using PCRL = Pws.Clients.RestLibrary;
using PCRLS = Pws.Clients.RestLibrary.Shared;
using PCRLE = Pws.Clients.RestLibrary.ECommerce;
using PCRLC = Pws.Clients.RestLibrary.Customers;
using PCRLP = Pws.Clients.RestLibrary.Products;
using PCRLPN = Pws.Clients.RestLibrary.Products.Navigation;
using PCRLW = Pws.Clients.RestLibrary.WorksurfaceJobs;
using PCRLWe = Pws.Clients.RestLibrary.WorksurfaceEstimator;
using PCRLSu = Pws.Clients.RestLibrary.Suppliers;

namespace Pws.Clients.RestExample.ConsoleApplication
{
	/*
	 * Copyright (C) PWS Distributors Ltd 2011-2014
	 * All rights reserved.
	 * 
	 */

	/// <summary>
	/// 
	/// PWS REST Web Services example
	/// Visual Studio C# solution
	/// 
	/// 
	/// This solution consists of four parts:
	/// 
	/// 
	/// 
	/// 1. Pws.Clients.RestLibrary (assembly)
	///    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	///    
	///    These are PWS business object representations, automatically serialisable to an Xml format which is compatible with the web services.
	///    Client not using Microsoft .Net can generate the Xml manually instead.
	/// 
	/// 
	/// 
	/// 2. Pws.Clients.RestLibrary.Service (assembly, source available)
	///    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	/// 
	///    This provides methods for transacting the business objects (from Pws.Clients.RestLibrary) to and from the Services.
	///    The main interfaces are IPwsApplication and IPwsService.
	/// 
	/// 
	/// 
	/// 3. Pws.Clients.RestLibrary.Transport (assembly, source available)
	///    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	///    
	///    This formats the web requests in a way that is compliant with the Services documentation.
	///    
	///    Clients using Microsoft .Net do not need to consider this namespace, as it's hidden by the Pws.Clients.RestLibrary.Service layer.
	///    Clients not using Microsoft .Net can use this source code as a starting point for creating their own code.
	/// 
	/// 
	/// 
	/// 4. Pws.Clients.RestExample.ConsoleApplication (source code)
	///    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	///    
	///    Makes use of the Pws.Clients.RestLibrary.Transport project, combined with Pws.Clients.RestLibrary assemblies,
	///    to execute some example Service calls.
	///    
	/// </summary>
	partial class Program
	{
		// The starting point is to create an instance of the IPwsService interface.
		// This provides the functions for transacting the PWS RestLibrary objects with the REST service,
		// in the raw form of Get/Put/Post/Delete of PWS objects.
		//
		// Typically only one instance of IPwsService ever needs to be created, as it can serve multiple users.
		static readonly IPwsService __service = new PwsService(Configuration.ApiKey, Configuration.PrivateKey, Configuration.DeploymentType);

		static void Main(string[] args)
		{
			try
			{
				// --------------------------------------------
				// 1.
				// For developers who don't want to understand the nuts and bolts...
				// A simple example of using the PWS REST Library to make requests to the main PWS REST service.
				QuickStart();


				// --------------------------------------------
				// 2.
				// Getting product information. This shows how to retrieve objects and lists of objects.
				// Updates are not demonstrated.
				DemonstrateProductInformation();


				// --------------------------------------------
				// 3.
				// Affecting sales orders. This shows how to retrieve and also update objects.
				// It also demonstrates retrieving customer specific price information.
				DemonstrateOrderOperations();


				// --------------------------------------------
				// 4.
				// Bespoke products.
				// These are the most complex products to order using the web service, because they require an interactive process of question
				// and response between the service and the user. However, it is abstracted in a way that avoids the client application
				// having the understand or reproduce the complex logic behind bespoke products, which also keeps the system future proofed.
				DemonstrateBespokeProducts();


				// --------------------------------------------
				// 5.
				// Navigating around the directory.
				// Based upon the catalogue information, this is an interactive service for helping a user to navigate around the catalogue
				// in a parameter-driven way, by iteratively filtering on categories of product.
				//
				// It can also be used for searching the product database, and discovering connections between variants of products.
				DemonstrateCatalogueNavigation();


				// --------------------------------------------
				// 6.
				// Navigating around the directory, with a view to pricing information.
				// Retrieving net prices for a customer; searching the product database; and discovering connections between variants of products.
				DemonstrateNetPricingRetrieval();


				// --------------------------------------------
				// 7.
				// Supplier operations.
				// Used by PWS Web Portal to allow suppliers to submit Template and Installation data and obtain a work list.
				DemonstrateSupplierOperations();
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.ToString());
			}

			Console.WriteLine("Done");
			Console.ReadKey();
		}


		/// <summary>
		/// A simple example of retrieving basic customer information and then submitting a new order.
		/// </summary>
		private static void QuickStart()
		{
			// ============================================================================
			// Basics of object retrieval and navigation.

			// Most REST operations can be performed using the supplied .NET wrapper classes,
			// which are a higher level abstraction built upon REST. It removes the needs to
			// understand the underlying REST Services mechanics.
			//
			// This uses the provided data representations in Pws.Clients.RestLibrary, and wraps them using
			// additional classes from Pws.Clients.RestLibrary.Service:
			// 
			// * IPwsApplication: the main factory for retrieving Tokens or default service objects. Consider instantiating one per user, if implementing a multi-user system such as a website.
			// * IPwsObjectWrapper: wraps the data representations with helper methods for following links to other objects, or for performing other actions such as updates.



			// In this case, the username and password will be supplied so that authentication Tokens are handled automatically.
			IPwsApplication application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// Most operations are done via the use of a Token. This represents authentication
			// to the PWS services, so that users cannot access information which is private
			// to others.
			//
			// The PwsApplication was created with the user's credentials passed in already, so it can obtain a token automatically.
			IPwsObjectWrapper<PCRLE.Users.Token_V1> token = application.GetDefaultToken();
			Console.WriteLine("Token: expires " + token.PwsObject.ExpiryTime);

			// An object can be followed down to its linked sub objects.
			// Each action of following a link implicitly calls the REST services, following the Uri within the object.
			// In this case, the Token will be followed down to its linked object of Customer.
			IPwsObjectWrapper<PCRLC.Customer_V1> customer = token.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer);
			Console.WriteLine("Customer: " + customer.PwsObject.AccountCode);

			// Some objects return a list of items.
			// This example shows how to retrieve the outstanding orders from the customer, first page of results only.
			IListPage<PCRLC.Orders.Order_V1> outstandingOrders = customer.FollowListPage<PCRLC.Orders.Order_V1>(f => f.OutstandingOrders);

			// Methods signatures are designed to be chained together, to make development more convenient.
			// For example, this will use the methods described above (Follow, FollowListPage) to retrieve a path of objects
			// from the Token through to the first outstanding order. Each method performs a retrieval from the REST Services.
			var someOrderInformation = token
				.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer)
				.FollowListPage<PCRLC.Orders.Order_V1>(f => f.OutstandingOrders)
				.ThisPage
				.FirstOrDefault();



			// ============================================================================
			// Stop at this point. Any calls further down are affective live data.
			// Halt in Debug mode, so that the developer has the opportunity to cancel.
			Debug.Fail("Remove this line to allow the example program to continue", "Any operations below this will be editing data, rather than just retrieving it.");



			// ============================================================================
			// Creating a sales order.


			// Post the new order to the customer, which will then create the order and return it with
			// all fields filled in.
			IPwsObjectWrapper<PCRLC.Orders.Order_V1> order = customer.Post(f => f.OutstandingOrders, new PCRLC.Orders.Order_V1()
			{
				OrderReference = "TEST" + DateTime.Now.ToString("yyyyMMddHHmmss")
			});

			// Declare some data to go on the order (instead of supplying the data inline, as per the above. Both are equivalent).
			PCRLC.Orders.OrderLine_V1 orderLineRequest = new PCRLC.Orders.OrderLine_V1()
			{
				ProductId = "3203",
				OrderQuantity = 100
			};

			// Send the line to the order.
			//
			// For this call, the example demonstrates Exception handling.
			// Clients should implement this on all service calls if possible, but certainly on mutable operations (i.e. ones which are changing the data via put/post/delete).
			try
			{
				IPwsObjectWrapper<PCRLC.Orders.OrderLine_V1> orderLine = order.Post(f => f.Lines, orderLineRequest);
				Console.WriteLine("Posted line: " + orderLine.PwsObject.LineTotal.AmountExcludingVat + " " + orderLine.PwsObject.Product.Title);
			}
			catch (PwsServiceConflictException ce)
			{
				// *** This exception is considered to be expected.

				// Because this is a multi-user system, it is possible that another user will have already started modifying
				// the order (albeit unlikely in this simple example).
				// Therefore, the client application must expect that the Post operation for the new line will fail, due to
				// the PWS system rejecting the update because another user has made a more recent change.

				// Obtain the latest version of the order; merge any changes; and then
				// re-post the line.
				order = order.Refresh();

				// Merge changes... Not implemented in this example, as it depends on the business logic
				// which the client is following.
				// For example, the client application might wish to re-retrieve the order again,
				// display it on screen to the end-user, and ask whether they still wish to post the new line.

				// Re-post the line. Ideally, this should be handled for exceptions too; but is omitted for clarity in this example.
				IPwsObjectWrapper<PCRLC.Orders.OrderLine_V1> orderLine = order.Post(f => f.Lines, orderLineRequest);
				Console.WriteLine("Posted line after having to refresh order: " + orderLine.PwsObject.LineTotal.AmountExcludingVat + " " + orderLine.PwsObject.Product.Title);
			}
			catch (PwsServiceAuthenticationException ae)
			{
				// *** This exception is considered to be expected, but is unlikely if using the default Token handling
				//     within the IPwsApplication instance.

				// The user's credentials are invalid or have expired. The client application might simply need to request a new Token
				// and re-try.
				Console.WriteLine("User authentication failed. " + ae.ToString());
				return;
			}
			catch (PwsServicePermissionException pe)
			{
				// *** This exception is considered to be expected.

				// The user's credentials do not permit them to perform the request. E.g. some users are not permitted to amend orders,
				// but they might be permitted to read them.
				Console.WriteLine("User is not permitted to do this. " + pe.ToString());
				return;
			}
			catch (PwsServiceValidationException ve)
			{
				// *** This exception is considered to be expected.

				// Although the order line might look valid, some business rules have been contravened. The user will need
				// to be presented with the message and must take action to correct the object before re-posting it.
				Console.WriteLine("Please correct the error in the posted values and re-submit them. " + ve.ToString());
				return;
			}
			catch (PwsServiceResponseException we)
			{
				// *** This exception is the result of a failure in the client or server, and should be considered as a program error.
				// *** Optional, providing that the program has appropriate error trapping higher up.

				// The service was successfully called but has returned an unexpected error.
				// There is no automated solution to resolving this.
				Console.WriteLine(we.ToString());
				return;
			}
			catch (PwsServiceException pse)
			{
				// *** This exception is the result of a failure in the client or server, and should be considered as a program error.
				// *** Optional, providing that the program has appropriate error trapping higher up.

				// A generic problem when calling the service, such as a timeout or lack of network connection.
				Console.WriteLine("Error when calling the service", pse.ToString());
				return;
			}

			// Any IPwsObjectWrapper.Post or .Put method will automatically refresh the object, so that the changes are obtained automatically.
			// I.e. because order.Post was called to create the new line, the order variable will already contain a refreshed copy of the order
			// with the new price etc.
			Console.WriteLine("Total after adding line: " + order.PwsObject.OrderTotal.AmountIncludingVat);



			// ============================================================================
			// Releasing the sales order for picking, including assigning credit.


			// Choose a delivery address for the customer.
			// Manual addresses are also possible: please see code examples later on in the full "DemonstrateOrderOperations()" method.
			IList<IPwsObjectWrapper<PCRLC.Address_V1>> customerAddresses = customer.FollowList<PCRLC.Address_V1>(f => f.Addresses);

			// In this case, assume the user wants the first address. NB, not all users have permission to change order addresses, so check permissions.
			if (token
				.FollowList<PCRLE.Permissions.Permission_V1>(f => f.GrantedPermissions)
				.Any(f => f.PwsObject.PermissionId.Contains("CustomerOrderAddressesAny")))
			{
				PCRLC.Address_V1 chosenAddress = customerAddresses.First().PwsObject;
				order.Put(f => f.DeliveryAddress, chosenAddress);
			}


			// There might be several order release methods for the customer, depending upon whether they're a credit or a cash customer.
			IList<IPwsObjectWrapper<PCRLC.Orders.Release_V1>> releaseMethods = order.FollowList<PCRLC.Orders.Release_V1>(f => f.AvailableReleaseMethods);

			// Once the user has chosen a method based on its description, post this back to the order to effect a release.
			// NB *** Not executed if using the Production server, for safety ***
			if (Configuration.DeploymentType != DeploymentType.Production)
			{
				// In this case, assume the user wants the first method that isn't a card payment.
				PCRLC.Orders.Release_V1 chosenReleaseMethod = releaseMethods.Where(f => f.PwsObject.IsExternalPaymentMethod == false).First().PwsObject;
				order.Post(f => f.ReleaseMethod, chosenReleaseMethod);
			}



			// ============================================================================
			// Cancel the order.


			// Exception handling demonstrated again, but without the comments.
			try
			{
				order.DeleteSelf();
				Console.WriteLine("Deleted order");
			}
			catch (PwsServiceConflictException ce)
			{
				Console.WriteLine(ce.Message);
			}
			catch (PwsServiceAuthenticationException ae)
			{
				Console.WriteLine(ae.Message);
			}
			catch (PwsServicePermissionException pe)
			{
				Console.WriteLine(pe.Message);
			}
			catch (PwsServiceValidationException ve)
			{
				Console.WriteLine(ve.Message);
			}
		}


		/// <summary>
		/// This example shows how to retrieve product information.
		/// It does not demonstrate how to perform updates.
		/// </summary>
		private static void DemonstrateProductInformation()
		{
			// ============================================================================
			// Option 1, customer net pricing as Xml export file.
			//
			// Retrieving product information in a very basic form via a single Xml file
			// of all products and net prices for a specific customer.
			// This is suitable for systems that need a periodic update of all net price catalogue information by customer.
			// Obviously, knowledge of the Xml file is required, which is outside of the scope of the REST Services.

			// Create the service instance, in this case with credentials for the required customer (or a superuser account for cross-customer requests).
			IPwsApplication application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// Obtain the net pricing information for all products saleable to that customer.
			// NB this call will take several minutes to execute.
			FileInfo xmlExport = application.GetDefaultToken()
				.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer)  // NB some superuser credentials permit querying a different customer's pricing; in which case, follow the SecondaryCustomer link with an OData query on AccountCode.
				.GetBinaryFile(f => f.ProductExportDocument);

			xmlExport.CopyTo(@"c:\temp\asd.xml", true);

			// If using a superuser login which has access to multiple customers, then this approach will retrieve a file per customer.
			// This may take several minutes per customer.
			IEnumerable<FileInfo> xmlExportAllCustomers = application.GetDefaultToken()
				.FollowList<PCRLC.Customer_V1>(f => f.SecondaryCustomers) // Get a list of all customers which can be accessed by this token credentials.
				.Take(2) // Take just a couple, for brevity.
				.Select(f => f.GetBinaryFile(g => g.ProductExportDocument)); // For each customer, request the pricing export.


			// ============================================================================
			// Option 2, general product information.
			//
			// Get a list of all products with their category information. Note that a token is not required (because this information
			// does not change depending upon the user login). However, no error will be raised if you do supply a token.

			// Create the service instance, in this case without any credentials.
			application = new PwsApplication(__service);

			// Get a wrapper to the main ProductStore object, against which the list of products can be retrieved.
			IPwsObjectWrapper<PCRLP.ProductStore_V1> productStore = application.GetRootService<PCRLP.ProductStore_V1>("productstore");

			// Retrieve the list of products; in this case, with the Category sub-object already filled out for convenience.
			// This is obtained by following the "ProductInformation" link.
			IList<IPwsObjectWrapper<PCRLP.ProductInformation_Category_V1V1>> productsWithAllCategories = productStore.FollowList<PCRLP.ProductInformation_Category_V1V1>(f => f.ProductInformation);


			// ============================================================================
			// Option 3, customer net pricing as objects.
			//

			// Create the service instance, in this case with credentials for the customer or the superuser account.
			application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// There are several ways of getting to the product pricing data (refer to documentation diagram).
			// In this case, follow the link from the Customer.
			var customer = application.GetDefaultToken()
				.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer);

			// Get all pricing. This may take some time, perhaps up to 10 minutes. It is fetched in pages, to avoid client timeouts.
			var productPrices = customer.FollowList<PCRLC.Products.Product_V1>(f => f.Products);
		}


		/// <summary>
		/// This example demonstrates many customer and user specific operations can be performed, using the user's security Token as the starting point.
		/// Some use of made of var instead of declaring the variable types explicitly; this is just for brevity.
		/// </summary>
		private static void DemonstrateOrderOperations()
		{
			// Create an IPwsApplication instance for this user, constructing it with their credentials for simplicity.
			IPwsApplication application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// Get the Token for the user (possible the credentials were supplied in the constructor).
			IPwsObjectWrapper<PCRLE.Users.Token_V1> token = application.GetDefaultToken();

			// Follow the permissions link to find out what this token is permitted to do.
			var tokenPermissions = token.FollowList<PCRLE.Permissions.Permission_V1>(f => f.GrantedPermissions);
			Console.WriteLine("Token permissions (count): " + tokenPermissions.Count());

			// Information about this user.
			IPwsObjectWrapper<PCRLE.Users.User_V1> user = token.Follow<PCRLE.Users.User_V1>(f => f.PrimaryUser);
			Console.WriteLine("User: " + user.PwsObject.UserId);

			// User's default permissions. Might be different from the Token, e.g. if permissions have been temporarily escalated within the token.
			var userPermissions = user.FollowList<PCRLE.Permissions.Permission_V1>(f => f.GrantedPermissions);
			Console.WriteLine("User default permissions (count): " + userPermissions.Count());

			// User's default denied permissions.
			var userDeniedPermissions = user.FollowList<PCRLE.Permissions.Permission_V1>(f => f.DeniedPermissions);
			Console.WriteLine("User denied permissions (count): " + userDeniedPermissions.Count());

			// Secondary users?
			if (userPermissions.Where(f => f.PwsObject.PermissionId.ToLower().Contains("usersecondary")).Any())
			{
				var secondaryUsers = token.FollowList<PCRLE.Users.User_V1>(f => f.SecondaryUsers);
				Console.WriteLine("Secondary users (count): " + secondaryUsers.Count());
				if (secondaryUsers.Any())
				{
					var secondaryUser = secondaryUsers.First();

					var secondaryUserPermissions = secondaryUser.FollowList<PCRLE.Permissions.Permission_V1>(f => f.GrantedPermissions);
					Console.WriteLine("Secondary users (first of), User default permissions (count): " + secondaryUserPermissions.Count());

					var secondaryUserDeniedPermissions = secondaryUser.FollowList<PCRLE.Permissions.Permission_V1>(f => f.DeniedPermissions);
					Console.WriteLine("Secondary users (first of), User denied permissions (count): " + secondaryUserDeniedPermissions.Count());

					var deniedPermission = secondaryUserDeniedPermissions.FirstOrDefault();
					if (deniedPermission != null)
					{
						Console.WriteLine("Secondary users (first of), currently denied permission: " + deniedPermission.PwsObject.Description);
						secondaryUser.Put<PCRLE.Permissions.Permission_V1>(f => f.GrantedPermissions, deniedPermission.PwsObject);

						secondaryUserDeniedPermissions = secondaryUser.FollowList<PCRLE.Permissions.Permission_V1>(f => f.DeniedPermissions);
						Console.WriteLine("Secondary users (first of), permission " + deniedPermission.PwsObject.Description + " no longer denied: " + (secondaryUserDeniedPermissions.Where(f => f.PwsObject.PermissionId == deniedPermission.PwsObject.PermissionId).Any() == false));

						secondaryUser.Delete(f => f.GrantedPermissions, deniedPermission.PwsObject);
						secondaryUserDeniedPermissions = secondaryUser.FollowList<PCRLE.Permissions.Permission_V1>(f => f.DeniedPermissions);
						Console.WriteLine("Secondary users (first of), permission " + deniedPermission.PwsObject.Description + " set to denied again: " + secondaryUserDeniedPermissions.Where(f => f.PwsObject.PermissionId == deniedPermission.PwsObject.PermissionId).Any());
					}
				}
			}



			// ============================================================================
			// Follow the PrimaryCustomer link, from the token, to obtain the Customer.
			IPwsObjectWrapper<PCRLC.Customer_V1> customer = token.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer);
			Console.WriteLine("Customer: " + customer.PwsObject.AccountCode);

			// Customer's previous orders, all pages. This might take some time for a customer with many previous orders.
			Uri nextPage = null;
			Uri previousPage = null;
			var previousOrders = customer.FollowListPage<PCRLC.Orders.Order_V1>(f => f.PreviousOrders);
			Console.WriteLine("Previous orders for " + customer.PwsObject.AccountCode + ", first page count: " + previousOrders.ThisPage.Count());
			Console.WriteLine("Previous orders, first page, all cancelled or completed: " + (previousOrders.ThisPage.Where(f => f.PwsObject.Status == "Outstanding").Any() == false));

			// Order retrieval using complex OData. In this case, getting all outstanding orders that are still editable by the client.
			var filteredOrders = customer.FollowList<PCRLC.Orders.Order_V1>(f => f.AllOrders, "top=10", "skip=10", "filter=Status Eq 'Outstanding' AND IsUpdateableByCustomer Eq True AND OrderDate Ge 2012-02-07T10:18:34 00:00", "orderby=OrderDate desc");
			Console.WriteLine("Current orders, all outstanding: " + (filteredOrders.Where(f => f.PwsObject.Status != "Outstanding").Any() == false));
			
			// Get feedback on the latest order's progression.
			IPwsObjectWrapper<PCRLC.Orders.Order_V1> progressionOrder = filteredOrders.FirstOrDefault();
			if (progressionOrder != null)
			{
				IPwsObjectWrapper<PCRLC.Orders.Progression_V1> progression2 = progressionOrder.Follow<PCRLC.Orders.Progression_V1>(f => f.Progression);
				foreach (PCRLC.Orders.Progression_V1.Fulfilment fulfilment in progression2.PwsObject.Fulfilments)
				{
					// There may be multiple fulfilments per order, for example if there is a backorder then a second fulfilment will exist.
					Console.WriteLine("Progression for " + fulfilment.Description + ": ");
					Console.WriteLine(String.Join(Environment.NewLine + "   ", fulfilment.Events.Select(f => f.Description).ToArray()));
				}
			}


			// This user may also have SecondaryCustomers, for instance if they are a sales rep.
			if (userPermissions.Where(f => f.PwsObject.PermissionId.ToLower().Contains("customerretrievesecondaryany")).Any())
			{
				var secondaryCustomers = token.FollowListPage<PCRLC.Customer_V1>(f => f.SecondaryCustomers);
				Console.WriteLine("Other customers, first page: " + String.Join(", ", secondaryCustomers.ThisPage.Select(f => f.PwsObject.AccountCode).ToArray()));
			}

			// List the primary customer's addresses.
			var customerAddresses = customer.FollowList<PCRLC.Address_V1>(f => f.Addresses);
			Console.WriteLine(String.Join(Environment.NewLine, customerAddresses.Select(f => f.PwsObject).Select(f => "Address " + f.AddressNumber + ": " + f.Name + " " + f.Postcode).ToArray()));

			// Get the country of the primary address.
			IPwsObjectWrapper<PCRLS.Country_V1> country = customerAddresses.First().Follow<PCRLS.Country_V1>(f => f.Country);
			Console.WriteLine(country.PwsObject.Name + " (" + country.PwsObject.Code + ")");

			// List the primary customer's contacts.
			var customerContacts = customer.FollowList<PCRLC.Contacts.Contact_V1>(f => f.Contacts);
			Console.WriteLine(String.Join(Environment.NewLine, customerContacts.Select(f => f.PwsObject).Select(f => "Contact " + f.ContactId + ": " + f.FirstName + " " + f.Surname).ToArray()));

			// Try to spoof accessing a random contact number.
			// Clients: ignore this, as it's just a test of the effectiveness of the security model.
			try
			{
				String uri = customerContacts.First().PwsObject.FindSelfLink().Href.OriginalString;
				uri = uri.Replace("/" + customerContacts.First().PwsObject.ContactId, "/12345");
				PCRLC.Contacts.Contact_V1 randomContact = __service.Get<PCRLC.Contacts.Contact_V1>(new Uri(uri), token.PwsObject);

				Console.WriteLine("Retrieving a random contact: this should only have passed if you are PWS employee!");
			}
			catch
			{
				Console.WriteLine("Retrieval of a random customer's contact details is not possible with your credentials (correct)");
			}

			// The customer's list of permitted countries that they can place orders to, for manual addresses:
			var countries = customer.FollowList<PCRLS.Country_V1>(f => f.AvailableCountries);
			Console.WriteLine("Permitted delivery countries, top 5: " + String.Join(", ", countries.Take(5).Select(f => f.PwsObject.Name).ToArray()));


			// ============================================================================
			// Get the product prices. Firstly, an entire page of product prices.
			var products = customer.FollowListPage<PCRLC.Products.Product_V1>(f => f.Products);
			Console.WriteLine("Prices (first page): count of " + products.ThisPage.Count);

			// Next, fetch a particular product price.
			products = customer.FollowListPage<PCRLC.Products.Product_V1>(f => f.Products, "filter=ProductId eq 'RABEP900/18'");
			foreach (PCRLC.Products.Product_V1 product in products.ThisPage.Select(f => f.PwsObject))
			{
				Console.WriteLine(product.ProductId + " [" + product.Description + "] " + product.Price.NetPrice + product.Price.CurrencyCode + " " + product.Price.AvailableQuantity + "x");
			}
			Console.WriteLine();


			// ============================================================================
			// View the lines on a predefined template.
			var predefinedTemplates = customer.FollowList<PCRLC.Orders.Order_V1>(f => f.PredefinedTemplates);
			var predefinedTemplatePainted = predefinedTemplates.Where(f => f.PwsObject.Title.ToLower().Contains("painted")).FirstOrDefault();
			if (predefinedTemplatePainted != null)
			{
				var paintedTemplateLines = predefinedTemplatePainted.FollowList<PCRLC.Orders.OrderLine_V1>(f => f.Lines);
				Console.WriteLine("Found " + paintedTemplateLines.Count() + " lines on a painted door range template.");
			}

			
			// ============================================================================
			// Follow the link to the financial info.
			IPwsObjectWrapper<PCRLC.FinancialInfo_V1> financial = customer.Follow<PCRLC.FinancialInfo_V1>(f => f.FinancialInfo);
			Console.WriteLine("Financial: " + financial.PwsObject.Balance + financial.PwsObject.CurrencyCode);

			// Get latest statement.
			PCRLC.FinancialInfo_V1.Statement statement = financial.PwsObject.Statements.FirstOrDefault();
			if (statement != null)
			{
				// This is a rare example of where the IPwsService needs to be used directly, because the IPwsObjectWrapper is not implemented on FinancialInfo_V1.Statement.
				FileInfo statementPdf = __service.GetBinaryFile(statement.StatementDocument.Href, statement.StatementDocument.MediaType, token.PwsObject);
				System.Diagnostics.Process.Start(statementPdf.FullName);
			}

			// Follow the links to financial overdue balance buckets.
			foreach (PCRLC.FinancialInfo_V1.BalanceOverdueBucket bucket in financial.PwsObject.BalanceOverdueBuckets)
			{
				// Output bucket and transaction info.
				Console.WriteLine("Bucket " + bucket.BucketReference + " " + bucket.BalanceOverdue + financial.PwsObject.CurrencyCode + " overdue, transactions:");

				// Get transactions associated with bucket.
				// This is a rare example of where the IPwsService needs to be used directly, because the IPwsObjectWrapper is not implemented on FinancialInfo_V1.BalanceOverdueBucket.
				IEnumerable<PCRLC.FinancialTransaction_V1> transactions = __service.GetList<PCRLC.FinancialTransaction_V1>(bucket.FinancialTransactions.Href, token.PwsObject);
				Console.WriteLine(String.Join(Environment.NewLine, transactions.Select(f => f.OriginalValue + " " + f.OutstandingValue + " " + f.Description + " " + (f.Document != null ? f.Document.Href.ToString() : String.Empty)).ToArray()));
			}

			// Follow the link to the undue balance.
			var undueTransactions = financial.FollowList<PCRLC.FinancialTransaction_V1>(f => f.UndueFinancialTransactions);
			Console.WriteLine("Undue transactions: " + String.Join(", ", undueTransactions.Select(f => f.PwsObject).Select(f => f.Reference + ":" + f.OutstandingValue).ToArray()));

			// Output the document, if supplied.
			if (undueTransactions.Any() && undueTransactions.First().PwsObject.Document != null)
			{
				FileInfo transactionPdf = undueTransactions.First().GetBinaryFile(f => f.Document);
				System.Diagnostics.Process.Start(transactionPdf.FullName);
			}


			// ============================================================================
			// Follow the link to the Contact Us information at PWS.
			var contactUsDepartments = customer.FollowList<PCRLC.Contacts.ContactUs_V1>(f => f.ContactUs);

			// Get a Contact Us contact.
			IPwsObjectWrapper<PCRLC.Contacts.ContactUs_V1> firstContactUsDepartment = contactUsDepartments.First();
			IPwsObjectWrapper<PCRLC.Contacts.Contact_V1> contact = firstContactUsDepartment.Follow<PCRLC.Contacts.Contact_V1>(f => f.Contact);
			Console.WriteLine("Contact us at department " + firstContactUsDepartment.PwsObject.Department + ": " + contact.PwsObject.FirstName + " " + contact.PwsObject.Surname + " " + contact.PwsObject.LandlineTelephone);




			// ============================================================================
			// Stop at this point. Any calls further down are affective live data.
			// Halt in Debug mode, so that the developer has the opportunity to cancel.
			Debug.Fail("Remove this line to allow the example program to continue", "Any operations below this will be editing data, rather than just retrieving it.");



			// ============================================================================
			// service.Post a new order.
			IPwsObjectWrapper<PCRLC.Orders.Order_V1> order = customer.Post(f => f.OutstandingOrders, new PCRLC.Orders.Order_V1()
			{
				OrderReference = "TEST" + DateTime.Now.ToString("yyyyMMddHHmmss")
			});
			Console.WriteLine("New order " + order.PwsObject.OrderReference + ": " + order.PwsObject.OrderId);

			// Update the buyer contact.
			order.Put(f => f.BuyerContact, customerContacts.First().PwsObject);

			// Add a line.
			IPwsObjectWrapper<PCRLC.Orders.OrderLine_V1> line = order.Post(f => f.Lines, new PCRLC.Orders.OrderLine_V1()
			{
				ProductId = "3203",
				OrderQuantity = 200
			});
			Console.WriteLine("New line: " + line.PwsObject.ProductId + " " + line.PwsObject.Price.GrossPrice + line.PwsObject.Price.CurrencyCode);

			// Change the line quantity to 2.
			line.PwsObject.OrderQuantity = 100;
			line.PutSelf();
			Console.WriteLine("Adjusted quantity to: " + line.PwsObject.OrderQuantity);

			// Refresh this copy of the order, to get latest pricing etc.
			order = order.Refresh();

			// Add a second line.
			line = order.Post(f => f.Lines, new PCRLC.Orders.OrderLine_V1()
			{
				ProductId = "0038",
				OrderQuantity = 80
			});
			Console.WriteLine("New line: " + line.PwsObject.ProductId + " " + line.PwsObject.Price.GrossPrice + line.PwsObject.Price.CurrencyCode);

			// Cancel the line.
			line.DeleteSelf();
			Console.WriteLine("Line deleted: " + line.PwsObject.ProductId);

			order = order.Refresh();

			// Confirm how many lines remain.
			var lines = order.FollowList<PCRLC.Orders.OrderLine_V1>(f => f.Lines);
			Console.WriteLine("Remaining lines: " + lines.Count());

			// Get any alert information from the order, e.g. warning of incompatible product.
			var alerts = order.FollowList<PCRLC.Orders.Alert_V1>(f => f.Alerts);
			Console.WriteLine("Alerts: " + String.Join("; ", alerts.Select(f => f.PwsObject.Message).ToArray()));

			// Try accepting an alert (if required).
			foreach (var alertToAcknowledge in alerts.Where(f => f.PwsObject.IsAcceptanceRequired))
			{
				Console.WriteLine("Acknowledging Alert (via delete) for: " + alertToAcknowledge.PwsObject.Message);
				alertToAcknowledge.DeleteSelf();
			}

			// If there are any template orders against this customer, then apply a template (i.e. all the lines from it) to this order.
			var templates = customer.FollowList<PCRLC.Orders.Order_V1>(f => f.Templates);
/*			if (templates.Any())
			{
				// Display lines that are about to be added from the template.
				lines = service.GetList<PCRLC.Orders.OrderLine_V1>(templates.Last().Lines.Href, token);
				Console.WriteLine("Adding template lines: " + String.Join(", ", lines.Select(f => f.ProductId).ToArray()));

				// Add lines.
				service.Post<PCRLC.Orders.Order_V1>(order.Template.Href, templates.Last(), token);
			}*/

			// If there are any predefined templates supplied by PWS, then apply a template.
			templates = customer.FollowList<PCRLC.Orders.Order_V1>(f => f.PredefinedTemplates);
/*			if (templates.Any())
			{
				// Display lines from thie template.
				lines = service.GetList<PCRLC.Orders.OrderLine_V1>(templates.Last().Lines.Href, token);
				Console.WriteLine("Adding predefined template lines: " + String.Join(", ", lines.Select(f => f.ProductId).ToArray()));

				// Add lines.
				service.Post<PCRLC.Orders.Order_V1>(order.Template.Href, templates.Last(), token);
			}*/

			templates = customer.FollowList<PCRLC.Orders.Order_V1>(f => f.PredefinedTemplates, "filter=OrderReference eq 'FancyLEDLightingPack'");
			

			// Refresh this copy of the order, to get latest pricing etc.
			order = order.Refresh();

			// Change the order reference.
			order.PwsObject.OrderReference += "A";
			order.PwsObject.Title = "Dummy title for test order " + order.PwsObject.OrderReference;
			order.PutSelf();

			// Show the delivery address.
			IPwsObjectWrapper<PCRLC.Address_V1> address = order.Follow<PCRLC.Address_V1>(f => f.DeliveryAddress);
			country = address.Follow<PCRLS.Country_V1>(f => f.Country);
			Console.WriteLine("Deliver to: " + address.PwsObject.Line1 + ", " + address.PwsObject.Line2 + ", " + address.PwsObject.Postcode + ", " + country.PwsObject.Name);

			// If permitted, put a different delivery address from the customer's predefined addresses.
			if (tokenPermissions.Where(f => f.PwsObject.PermissionId.Contains("CustomerOrderAddressesAny")).Any())
			{
				address = customerAddresses.First();
				order.Put(f => f.DeliveryAddress, address.PwsObject);
			}

			// If permitted, put a manual delivery address to the order.
			if (tokenPermissions.Where(f => f.PwsObject.PermissionId.Contains("CustomerOrderAddressesFree")).Any())
			{
				PCRLC.Address_V1 newAddress = new PCRLC.Address_V1()
				{
					AccountCode = String.Empty,
					AddressNumber = String.Empty,
					Name = "Test, DO NOT DELIVER",
					Line1 = "Test",
					Line2 = "Test",
					Line3 = "Test",
					Line4 = "Test",
					Postcode = "DL5 6XJ"
				};
				address = order.Put(f => f.DeliveryAddress, newAddress);

				// Update the country.
				country = countries.Where(f => f.PwsObject.Code == "CH").First();
				address.Put<PCRLS.Country_V1>(f => f.Country, country.PwsObject);
			}


			// Get the list of predefined delivery addresses which have been loaded for this customer.
			var deliveryAddresses = customer.FollowList<PCRLC.Address_V1>(f => f.Addresses);

			// Find the address which corresponds to an ID of 000 - this is usually the invoice address, and might not work for your customer.
			IPwsObjectWrapper<PCRLC.Address_V1> selectedDeliveryAddress = deliveryAddresses.Where(f => f.PwsObject.AddressNumber == "000").First();

			// Put the selected address to the order, to set the delivery address on that order.
			selectedDeliveryAddress = order.Put(f => f.DeliveryAddress, selectedDeliveryAddress.PwsObject);

			// Get the order back again with its updated list of dispatch methods.
			order = order.Refresh();


			// Get the possible dispatch methods for this order type and delivery address.
			var dispatchMethods = order.FollowList<PCRLC.Orders.DispatchMethod_V1>(f => f.AvailableDispatchMethods);
			Console.WriteLine("Dispatch methods available: " + String.Join(", ", dispatchMethods.Select(f => f.PwsObject.Description).ToArray()));

			// Put a dispatch method to the order, to confirm the required method.
//			foreach (IPwsObjectWrapper<PCRLC.Orders.DispatchMethod_V1> method in dispatchMethods)
//			{
//			    Console.WriteLine("Changing dispatch method to " + method.PwsObject.Description);
//				order.Put<PCRLC.Orders.DispatchMethod_V1>(f => f.SelectedDispatchMethod, method.PwsObject);
//			}

			// Get the release methods, for making payment and releasing the order for distribution.
			var releaseMethods = order.FollowList<PCRLC.Orders.Release_V1>(f => f.AvailableReleaseMethods);
			Console.WriteLine("Release methods:");
			Console.WriteLine(String.Join(Environment.NewLine + "   ", releaseMethods.Select(f => f.PwsObject.ReleaseDescription).ToArray()));

			// service.Post a release method back, to instruct that the order should be released for distribution.
			// NB *** Not executed if using the Production server, for safety ***
			if (Configuration.DeploymentType != DeploymentType.Production && releaseMethods.Count() > 0)
			{
				// In this case, assume the user wants the first method.
				PCRLC.Orders.Release_V1 chosenReleaseMethod = releaseMethods.Where(f => f.PwsObject.IsExternalPaymentMethod).First().PwsObject;
				order.Post(f => f.ReleaseMethod, chosenReleaseMethod);
			}

			// Get feedback on the order's progression. In this test, because the order has only just been created, this will be limited.
			IPwsObjectWrapper<PCRLC.Orders.Progression_V1> progression = order.Follow<PCRLC.Orders.Progression_V1>(f => f.Progression);
			foreach (PCRLC.Orders.Progression_V1.Fulfilment fulfilment in progression.PwsObject.Fulfilments)
			{
				// There may be multiple fulfilments per order, for example if there is a backorder then a second fulfilment will exist.
				Console.WriteLine("Progression for " + fulfilment.Description + ": ");
				Console.WriteLine(String.Join(Environment.NewLine + "   ", fulfilment.Events.Select(f => f.Description).ToArray()));
			}

			// Post the order to the templates, to create it as a template for future use.
			order.PwsObject.OrderReference = "TEMP" + DateTime.Now.ToString("yyyyMMddHHmmss");
			IPwsObjectWrapper<PCRLC.Orders.Order_V1> template = customer.Post(f => f.Templates, order.PwsObject);

			// Delete the template.
			template.DeleteSelf();

			// Delete the order.
			order.DeleteSelf();




			// ============================================================================
			// Add a new contact to the primary customer.
			IPwsObjectWrapper<PCRLC.Contacts.Contact_V1> newContact = customer.Post(f => f.Contacts, new PCRLC.Contacts.Contact_V1()
			{
				HasResponsibilityInformationTechnology = true,
				Title = "Mr",
				FirstName = "New",
				Surname = "Person",
				Email = new Uri(@"mailto://systems.development@pws.co.uk")
			});
			Console.WriteLine("Added contact: " + newContact.PwsObject.FirstName + " " + newContact.PwsObject.Surname + " to address " + newContact.PwsObject.Address.Title);

			// Move the contact to another address, by putting a new address into the contact.
			IPwsObjectWrapper<PCRLC.Address_V1> newContactAddress = customerAddresses.Skip(1).FirstOrDefault();
			if (newContactAddress != null)
			{
				newContact.Put(f => f.Address, newContactAddress.PwsObject);
				Console.WriteLine("Moved contact to address " + newContact.PwsObject.Address.Title);
			}

			// Edit the new contact.
			newContact.PwsObject.FirstName = "NearlyNew";
			newContact.PutSelf();
			Console.WriteLine("Edit contact: " + newContact.PwsObject.FirstName + " " + newContact.PwsObject.Surname);

			// Get the list of contacts again, to see if the new contact is in there.
			customerContacts = customer.FollowList<PCRLC.Contacts.Contact_V1>(f => f.Contacts);

			// Create a new ECommerce user from the new contact.
/*			if (userPermissions.Where(f => f.PermissionId.ToLower().Contains("usercreate")).Any())
			{
				PCRLE.Users.User_V1 newUser = service.Post<PCRLC.Contacts.Contact_V1, PCRLE.Users.User_V1>(token.SecondaryUsers.Href, newContact, token);

				// List the Secondary users.
				IEnumerable<PCRLE.Users.User_V1> users = service.GetList<PCRLE.Users.User_V1>(token.SecondaryUsers.Href, token);

				// service.Delete the new user.
				service.Delete(newUser.Links.Where(f => f.Rel == "self").First().Href, newUser, token);

				// Check 1 users has been removed.
				IEnumerable<PCRLE.Users.User_V1> users1 = service.GetList<PCRLE.Users.User_V1>(token.SecondaryUsers.Href, token);
			}*/

			// service.Delete the new contact.
			newContact.DeleteSelf();

			// Get the list of contacts that have an ECommerce account.
			customerContacts = customer.FollowList<PCRLC.Contacts.Contact_V1>(f => f.Contacts, "filter=IsLinkedToECommerceUser eq True");
		}


		/// <summary>
		/// Some PWS product ranges are based on bespoke products. This example assumes that the developer is already familiar
		/// with the process of submitting a normal order line to an order.
		/// 
		/// With bespoke products, the POST of the order line will be rejected, and instead a structure will be passed back
		/// which includes a question that the user needs to answer, which is phrased as a multiple choice. The client
		/// application then attempts to POST the line again, this time with the user's answer provided as well.
		/// The process iterates until no more questions are asked and the line is successfully posted.
		/// 
		/// An alternative method permits use of Virtual Product codes, which are predefined combinations of bespoke product.
		/// </summary>
		private static void DemonstrateBespokeProducts()
		{
			IPwsApplication application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// Get customer.
			IPwsObjectWrapper<PCRLC.Customer_V1> customer = application.GetDefaultToken()
				.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer);

			// Create a new order. Fill in some required details first.
			PCRLC.Orders.Order_V1 orderRequest = new PCRLC.Orders.Order_V1()
			{
				OrderReference = "TEST" + DateTime.Now.ToString("yyyyMMddHHmmss")
			};

			// Post order.
			IPwsObjectWrapper<PCRLC.Orders.Order_V1> order = customer.Post(f => f.OutstandingOrders, orderRequest);
			Console.WriteLine("Created order: " + order.PwsObject.OrderId);



			// ============================================================================
			// Using Question and Answer interactive approach. This is used by PWS Tradezone,
			// but perhaps isn't appropriate for client programs that take a batch approach.

			// Add line which happens to be a bespoke product. The client application doesn't need to have knowledge of this being a
			// bespoke product, so just posts it in the normal way.
			// NB: clients implementing bespoke products must implement V2 of OrderLine. This can be used for both bespoke products
			// and normal products.
			IPwsObjectWrapper<PCRLC.Orders.OrderLine_V2> line = order.Post(f => f.Lines, new PCRLC.Orders.OrderLine_V2()
			{
				ProductId = "S9DS770X300",
				OrderQuantity = 2,
				IgnoreBespokeOptionDefaults = true
			});

			while (line.PwsObject.NextBespokeOption != null)
			{
				Console.WriteLine(line.PwsObject.NextBespokeOption.Description + ":" + String.Join(String.Empty, line.PwsObject.NextBespokeOption.Options.Select(f => Environment.NewLine + "\t" + f.ProductId + ", " + f.Description).ToArray()));

				// Make a selection for this bespoke option. Just take one (the first, in this case).
				line.PwsObject.NextBespokeOption.Selection = line.PwsObject.NextBespokeOption.Options.First();
				line = order.Post(f => f.Lines, line.PwsObject);
			}

			if (line.PwsObject.PreviousBespokeOptions != null)
			{
				Console.WriteLine("Created new bespoke " + line.PwsObject.ProductId + ": " + line.PwsObject.Description);
				Console.WriteLine("Options confirmed in total: " + String.Join(String.Empty, line.PwsObject.PreviousBespokeOptions.Select(f => Environment.NewLine + "\t" + f.Selection.ProductId + (f.IsFromDefault ? " (from defaults)" : String.Empty)).ToArray()));
			}

			Console.WriteLine();

			// Try putting a bespoke line on again. This time, it should automatically apply the defaults, and so ask fewer questions.
			line = order.Post(f => f.Lines, new PCRLC.Orders.OrderLine_V2()
			{
				ProductId = "S9FG950X400",
				OrderQuantity = 1
			});

			while (line.PwsObject.NextBespokeOption != null)
			{
				Console.WriteLine(line.PwsObject.NextBespokeOption.Description);

				// Make a selection for this bespoke option. Just take one (the last, in this case).
				line.PwsObject.NextBespokeOption.Selection = line.PwsObject.NextBespokeOption.Options.Last();
				line = order.Post(f => f.Lines, line.PwsObject);
			}

			if (line.PwsObject.PreviousBespokeOptions != null)
			{
				Console.WriteLine("Created new bespoke " + line.PwsObject.ProductId + ": " + line.PwsObject.Description);
				Console.WriteLine("Options confirmed in total: " + String.Join(String.Empty, line.PwsObject.PreviousBespokeOptions.Select(f => Environment.NewLine + "\t" + f.Selection.ProductId + (f.IsFromDefault ? " (from defaults)" : String.Empty)).ToArray()));
			}
			
			Console.WriteLine();

			// Try putting a bespoke line on again which can have all its options completed by defaults, hence should add extra "confirm" question.
			line = order.Post(f => f.Lines, new PCRLC.Orders.OrderLine_V2()
			{
				ProductId = "S9C30",
				OrderQuantity = 1
			});

			while (line.PwsObject.NextBespokeOption != null)
			{
				Console.WriteLine(line.PwsObject.NextBespokeOption.Description);

				// Make a selection for this bespoke option. Just take one (the last, in this case).
				line.PwsObject.NextBespokeOption.Selection = line.PwsObject.NextBespokeOption.Options.Last();
				line = order.Post(f => f.Lines, line.PwsObject);
			}

			if (line.PwsObject.PreviousBespokeOptions != null)
			{
				Console.WriteLine("Created new bespoke " + line.PwsObject.ProductId + ": " + line.PwsObject.Description);
				Console.WriteLine("Options confirmed in total: " + String.Join(String.Empty, line.PwsObject.PreviousBespokeOptions.Select(f => Environment.NewLine + "\t" + f.Selection.ProductId + (f.IsFromDefault ? " (from defaults)" : String.Empty)).ToArray()));
			}

			Console.WriteLine();


			// A fully bespoke product which requires quantities to be entered.
			line = order.Post(f => f.Lines, new PCRLC.Orders.OrderLine_V2()
			{
				ProductId = "S9BESPOKE",
				OrderQuantity = 1
			});

			line.PwsObject.NextBespokeOption.Selection = line.PwsObject.NextBespokeOption.Options.Where(f => f.OptionCode == "BDRS9DS").First();
			line = order.Post(f => f.Lines, line.PwsObject);

			line.PwsObject.NextBespokeOption.Selection.Quantity = 300;
			line = order.Post(f => f.Lines, line.PwsObject);

			line.PwsObject.NextBespokeOption.Selection.Quantity = 330;
			line = order.Post(f => f.Lines, line.PwsObject);

			if (line.PwsObject.PreviousBespokeOptions != null)
			{
				Console.WriteLine("Created new bespoke " + line.PwsObject.ProductId + ": " + line.PwsObject.Description);
				Console.WriteLine("Options confirmed in total: " + String.Join(String.Empty, line.PwsObject.PreviousBespokeOptions.Select(f => Environment.NewLine + "\t" + f.Selection.ProductId + (f.IsFromDefault ? " (from defaults)" : String.Empty)).ToArray()));
				Console.WriteLine("£" + line.PwsObject.Price.GrossPrice.ToString("F2"));
			}



			// ============================================================================
			// Using a Virtual Product code, which encodes all the options within it, so
			// is more suitable for batch processing clients. This does require previous
			// knowledge of all Virtual Product codes and combinations, which is best
			// obtained from the Xml export (see "DemonstrateProductInformation()" method).

			// A fully bespoke product via a Virtual Product code.
			// Because this particular Virtual Product makes reference to Height and Width, need to send those values across at the same time.
			// Most Virtual Product codes are entirely self-contained, and don't require these extra parameters.
			List<PCRLC.Orders.OrderLine_V2.BespokeQuantityDefault> quantities = new List<PCRLC.Orders.OrderLine_V2.BespokeQuantityDefault>();
			quantities.Add(new PCRLC.Orders.OrderLine_V2.BespokeQuantityDefault() { OptionCode = "BESPOKES9WIDTH", Quantity = 500 });
			quantities.Add(new PCRLC.Orders.OrderLine_V2.BespokeQuantityDefault() { OptionCode = "BESPOKES9HEIGHT", Quantity = 500 });

			// Create the bespoke product including all its embedded options.
			line = order.Post(f => f.Lines, new PCRLC.Orders.OrderLine_V2()
			{
				ProductId = "XS9EQAIRAEAYC", // Corresponds to an S9BESPOKECD in a particular colour etc.
				OrderQuantity = 1,
				BespokeQuantityDefaults = quantities // Specifies the extra quantities required for this fully bespoke product.
			});

			Console.WriteLine("Created new bespoke " + line.PwsObject.ProductId + ": " + line.PwsObject.Description);
			Console.WriteLine("Options confirmed in total: " + String.Join(String.Empty, line.PwsObject.PreviousBespokeOptions.Select(f => Environment.NewLine + "\t" + f.Selection.ProductId + (f.IsFromDefault ? " (from defaults)" : String.Empty)).ToArray()));
			Console.WriteLine("£" + line.PwsObject.Price.GrossPrice.ToString("F2"));


			order.DeleteSelf();
		}


		/// <summary>
		/// Based upon the catalogue information, this is an interactive service for helping a user to navigate around the catalogue
		/// in a parameter-driven way, by iteratively filtering on categories of product.
		/// </summary>
		private static void DemonstrateCatalogueNavigation()
		{
			IPwsApplication application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// Get initial catalogue navigation choice, personalised for this customer and login.
			// (NB if nobody is logged in, then this information can be obtained via an anonymous call from the root Services object instead).
			// This initial object contains all navigation filters for the full range of available products.
			IPwsObjectWrapper<PCRLPN.NavigationChoice_V1> navigationChoice = application.GetDefaultToken()
				.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer)
				.Follow<PCRLPN.NavigationChoice_V1>(f => f.ProductNavigation);

			// A list of metadata for the ecommerce system's image feed. This may not be required for most clients.
			var images = application.GetRootService<Pws.Clients.RestLibrary.Framework.ECommerceMetadata_V1>("ecommercemetadata")
				.FollowList<Pws.Clients.RestLibrary.Framework.ECommerceMediaItem_V1>(f => f.Media);

			// Show initial navigation options:
			Console.WriteLine("Starting with " + navigationChoice.PwsObject.NextChoices.Count + " filters available from " + navigationChoice.PwsObject.ProductListCount + " products. List of filter classifications:" + Environment.NewLine);
			foreach (PCRLPN.NavigationChoice_V1.NextClassificationChoice classification in navigationChoice.PwsObject.NextChoices)
			{
				Console.WriteLine(classification.ClassificationName + ": " + classification.Categories.Count + " categories.");
			}
			Console.WriteLine();

			// Pretend that the user is following a journey through the choices available to them. Encode a set of simulated user clicks as if using the website:
			var userJourney = new[]
				{
					new { Mode = "select", Classification = "Domain", Category = "Fascia" }, // User filters by Domain = Fascia.
					new { Mode = "select", Classification = "Application", Category = "Door" }, // User filters by Application = Door.
					new { Mode = "select", Classification = "Colour", Category = "Alabaster" }, // User filters the product subset to Colour = Alabaster (in addition to Domain = Fascia and Application = Door).
					new { Mode = "undo", Classification = "Application", Category = "Door" }, // User removes the Application = Door filter, leaving an effective filter of just Domain = Fascia and Colour = Alabaster.
					new { Mode = "select", Classification = "Width", Category = "597mm" } // User filters the subset by Width = 597mm, leaving an effective filter of Domain = Fascia and Colour = Alabster and Width = 597mm.
				};

			// Each iteration might represent a user clicking on a filter option.
			foreach (var userChoice in userJourney)
			{
				Console.Write(userChoice.Mode + " " + userChoice.Classification + ":" + userChoice.Category + " from a possible " + navigationChoice.PwsObject.NextChoices.Count + " filters against " + navigationChoice.PwsObject.ProductListCount + " products => ");

				// For the user's Classification and Category choice, find the corresponding link which the PWS REST services has given for that filter choice.
				if (userChoice.Mode == "select")
				{
					// User is selecting a filter. Follow the link represented by that filter's Classification and Category.
					PCRLPN.NavigationChoice_V1.NextClassificationChoice nextClassificationChoice = navigationChoice.PwsObject.NextChoices.Where(f => String.Equals(f.ClassificationName, userChoice.Classification, StringComparison.CurrentCultureIgnoreCase)).Single();
					PCRLPN.NavigationChoice_V1.NextCategoryChoice nextCategoryChoice = nextClassificationChoice.Categories.Where(f => String.Equals(f.CategoryName, userChoice.Category, StringComparison.CurrentCultureIgnoreCase)).Single();
					PCRL.Link_V1 linkToNextFilterChoice = nextCategoryChoice.NavigationChoice;

					// Get the next navigation choice representing the new set of filters.
					navigationChoice = navigationChoice.Follow<PCRLPN.NavigationChoice_V1>(f => linkToNextFilterChoice);
				}
				else
				{
					// User is de-selecting a previously chosen filter. Follow the link represented by that filter's Classification (and implicitly Category).
					PCRLPN.NavigationChoice_V1.PreviousClassificationChoice undoClassificationChoice = navigationChoice.PwsObject.PreviousChoices.Where(f => String.Equals(f.ClassificationName, userChoice.Classification, StringComparison.CurrentCultureIgnoreCase)).Single();
					PCRL.Link_V1 linkToUndoFilterChoice = undoClassificationChoice.Category.NavigationChoice;

					// Re-calculate the navigation choice by removing a filter.
					navigationChoice = navigationChoice.Follow<PCRLPN.NavigationChoice_V1>(f => linkToUndoFilterChoice);
				}

				Console.Write(navigationChoice.PwsObject.ProductListCount + " products, " + navigationChoice.PwsObject.NextChoices.Count + " filters available.");

				// Retrieve the list of products matching what has been filtered so far - this is just to test the server performance; most clients wouldn't retrieve a full product list each time.
				var productListQuickCheck = navigationChoice.FollowList<PCRLP.ProductInformation_V1>(f => f.ProductList);
				Console.Write(" (" + productListQuickCheck.Count + ")");

				// Sometimes a position within the navigation structure might yield a cross-navigational link, e.g. "have you thought about..."
				if (navigationChoice.PwsObject.PromotionalChoices.Any())
				{
					Console.Write(" +promo " + String.Join(", ",
						navigationChoice.PwsObject.PromotionalChoices // May be multiple promotional choices.
						.Select(f => navigationChoice.Follow<PCRLPN.NavigationChoice_V1>(g => f.NavigationChoice)) // For each, fetch the cross-linked NavigationChoice object (for demonstration only; not typically required unless the user selects it).
						.Select(f => f.FollowList<PCRLP.ProductInformation_V1>(g => g.ProductList).Select(g => g.PwsObject.ProductId).First() + "...") // Display the first product in the cross-linked nav; again for demonstration only.
						.ToArray())
						);
				}

				Console.WriteLine();
			}

			// Summary of where the user has navigated to so far.
			Console.WriteLine(Environment.NewLine + "You have navigated to: " + navigationChoice.PwsObject.Name + " - " + navigationChoice.PwsObject.Description);

			// Finally, retrieve the list of products matching what has been filtered so far.
			var productList = navigationChoice.FollowList<PCRLP.ProductInformation_V1>(f => f.ProductList);
			Console.WriteLine(Environment.NewLine + "Retrieved " + productList.Count + " full product entries based on expected count of " + navigationChoice.PwsObject.ProductListCount);

			// Show more product info for the first 8 products by retrieving the category information and pricing.
			foreach (var product in productList.Take(Math.Min(8, productList.Count)))
			{
				var categories = product.FollowList<PCRLP.Category_V1>(f => f.Categories);
				Console.Write(product.PwsObject.Name + ": " + product.PwsObject.ProductId + " " + product.PwsObject.Description + " +" + categories.Count() + " categories, ");

				var sellingProduct = product.Follow<PCRLC.Products.Product_V1>(f => f.CustomerProduct);
				Console.WriteLine(sellingProduct.PwsObject.Price.NetPrice + " " + sellingProduct.PwsObject.Price.CurrencyCode + ", " + sellingProduct.PwsObject.Price.AvailableQuantity + "x");
			}

			// Show the first 4 variants that are linked to this product, with pricing information pre-filled.
			Console.WriteLine("Choosing a product to retrieve variants for: " + productList.First().PwsObject.ProductId);
			var variants = productList.First().FollowList<PCRLP.DifferentialProductInformation_CustomerProduct_V1V1>(f => f.Variants);
			Console.WriteLine(String.Join(", ", variants
				.Select(f => "[" + f.PwsObject.ProductId + ":" + String.Join(", ", f.PwsObject.DifferentialProductInfo.Select(g => g.DifferentialProductId + " " + g.DifferentialDescription).ToArray()) + "]")
				.Take(Math.Min(4, variants.Count()))
				.ToArray()));

			Console.WriteLine();
			

			// ============================================================================
			// All NavigationChoice objects have a search endpoint, provided by POSTing a NavigationChoice object back to the Searches link.

			// This is done within the context of the NavigationChoice object (i.e. can perform a search filter within the subset of products contained in a given NavigationChoice).
			// I.e. can search against the subset of products represented by the current NavigationChoice, as follows:
			navigationChoice = navigationChoice.Post<PCRLPN.NavigationChoice_V1>(f => f.Searches, new PCRLPN.NavigationChoice_V1()
			{
				RequestedSearch = new PCRLPN.NavigationChoice_V1.SearchTerms()
				{
					Keywords = new List<string>() { productList.First().PwsObject.ProductId } // Dummy search - look for a given product ID that's known to be in the current NavigationChoice.
				}
			});

			Console.WriteLine("Result of searching for a product like " + productList.First().PwsObject.ProductId + " within current NavigationChoice position: NavigationChoice with " + navigationChoice.PwsObject.ProductListCount + " product." + Environment.NewLine);

			// To search against all products, rather than a subset represented by the current NavigationChoice object, re-retrieve the root NavigationChoice object first.
			Console.Write("Result of searching for a product like 3203 from the intial Root NavigationChoice, i.e. all products: ");
			navigationChoice = application.GetRootService<PCRLPN.NavigationChoice_V1>("navigationchoice");
			navigationChoice = navigationChoice.Post<PCRLPN.NavigationChoice_V1>(f => f.Searches, new PCRLPN.NavigationChoice_V1()
			{
				RequestedSearch = new PCRLPN.NavigationChoice_V1.SearchTerms()
				{
					Keywords = new List<string>() { "3203" }
				}
			});
			Console.WriteLine(navigationChoice.PwsObject.ProductListCount + " product." + Environment.NewLine);

			Console.WriteLine();
			

			// ============================================================================
			// It is also possible to use the navigation service anonymously, i.e. without credentials and without a login token.
			Console.WriteLine("Now using an anonymous retrieval of NavigationChoice, instead of via a user token.");

			// New PwsApplication instance without any credentials or tokens.
			application = new PwsApplication(__service);

			// Retrieve anonymous initial navigation choice object.
			navigationChoice = application.GetRootService<PCRLPN.NavigationChoice_V1>("navigationchoice");
			Console.WriteLine("Initial object: " + navigationChoice.PwsObject.ProductListCount + " products, " + navigationChoice.PwsObject.NextChoices.Count + " filters available.");

			// Each iteration might represent a user clicking on a filter option.
			foreach (var userChoice in userJourney)
			{
				Console.Write(userChoice.Mode + " " + userChoice.Classification + ":" + userChoice.Category + " from a possible " + navigationChoice.PwsObject.NextChoices.Count + " filters against " + navigationChoice.PwsObject.ProductListCount + " products => ");

				// For the user's Classification and Category choice, find the corresponding link which the PWS REST services has given for that filter choice.
				if (userChoice.Mode == "select")
				{
					// User is selecting a filter. Follow the link represented by that filter's Classification and Category.
					PCRLPN.NavigationChoice_V1.NextClassificationChoice nextClassificationChoice = navigationChoice.PwsObject.NextChoices.Where(f => String.Equals(f.ClassificationName, userChoice.Classification, StringComparison.CurrentCultureIgnoreCase)).Single();
					PCRLPN.NavigationChoice_V1.NextCategoryChoice nextCategoryChoice = nextClassificationChoice.Categories.Where(f => String.Equals(f.CategoryName, userChoice.Category, StringComparison.CurrentCultureIgnoreCase)).Single();
					PCRL.Link_V1 linkToNextFilterChoice = nextCategoryChoice.NavigationChoice;

					// Get the next navigation choice representing the new set of filters.
					navigationChoice = navigationChoice.Follow<PCRLPN.NavigationChoice_V1>(f => linkToNextFilterChoice);
				}
				else
				{
					// User is de-selecting a previously chosen filter. Follow the link represented by that filter's Classification (and implicitly Category).
					PCRLPN.NavigationChoice_V1.PreviousClassificationChoice undoClassificationChoice = navigationChoice.PwsObject.PreviousChoices.Where(f => String.Equals(f.ClassificationName, userChoice.Classification, StringComparison.CurrentCultureIgnoreCase)).Single();
					PCRL.Link_V1 linkToUndoFilterChoice = undoClassificationChoice.Category.NavigationChoice;

					// Re-calculate the navigation choice by removing a filter.
					navigationChoice = navigationChoice.Follow<PCRLPN.NavigationChoice_V1>(f => linkToUndoFilterChoice);
				}

				Console.Write(navigationChoice.PwsObject.ProductListCount + " products, " + navigationChoice.PwsObject.NextChoices.Count + " filters available.");

				// Retrieve the list of products matching what has been filtered so far - this is just to test the server performance; most clients wouldn't retrieve a full product list each time.
				var productListQuickCheck = navigationChoice.FollowList<PCRLP.ProductInformation_V1>(f => f.ProductList);
				Console.Write(" (" + productListQuickCheck.Count + ")");

				// Sometimes a position within the navigation structure might yield a cross-navigational link, e.g. "have you thought about..."
				if (navigationChoice.PwsObject.PromotionalChoices.Any())
				{
					Console.Write(" +promo " + String.Join(", ",
						navigationChoice.PwsObject.PromotionalChoices // May be multiple promotional choices.
						.Select(f => navigationChoice.Follow<PCRLPN.NavigationChoice_V1>(g => f.NavigationChoice)) // For each, fetch the cross-linked NavigationChoice object (for demonstration only; not typically required unless the user selects it).
						.Select(f => f.FollowList<PCRLP.ProductInformation_V1>(g => g.ProductList).Select(g => g.PwsObject.ProductId).First() + "...") // Display the first product in the cross-linked nav; again for demonstration only.
						.ToArray())
						);
				}

				Console.WriteLine();
			}

			// Summary of where the user has navigated to so far.
			Console.WriteLine(Environment.NewLine + "You have navigated to: " + navigationChoice.PwsObject.Name + " - " + navigationChoice.PwsObject.Description);

			// Finally, retrieve the list of products matching what has been filtered so far.
			productList = navigationChoice.FollowList<PCRLP.ProductInformation_V1>(f => f.ProductList);
			Console.WriteLine(Environment.NewLine + "Retrieved " + productList.Count + " full product entries based on expected count of " + navigationChoice.PwsObject.ProductListCount);
			Console.WriteLine();

			// No pricing information will be available for these products, because it's an anonymous retrieval.

			// Show the first 4 variants that are linked to this product, without CustomerProduct pricing information because anonymous. I.e. could equally just retrieve DifferentialProductInformation_V1
			Console.WriteLine("Choosing a product to retrieve variants for: " + productList.First().PwsObject.ProductId);
			variants = productList.First().FollowList<PCRLP.DifferentialProductInformation_CustomerProduct_V1V1>(f => f.Variants);
			Console.WriteLine(String.Join(", ", variants
				.Select(f => "[" + f.PwsObject.ProductId + ":" + String.Join(", ", f.PwsObject.DifferentialProductInfo.Select(g => g.DifferentialProductId + " " + g.DifferentialDescription).ToArray()) + "]")
				.Take(Math.Min(4, variants.Count()))
				.ToArray()));

			Console.WriteLine();

			// Similarly, first 4 related products that are linked to this product. Don't bother retrieving the pricing.
			Console.WriteLine("Choosing a product to retrieve related for: " + productList.First().PwsObject.ProductId);
			var related = productList.First().FollowList<PCRLP.DifferentialProductInformation_V1>(f => f.RelatedProducts);
			Console.WriteLine(String.Join(", ", related
				.Select(f => "[" + f.PwsObject.ProductId + ":" + String.Join(", ", f.PwsObject.DifferentialProductInfo.Select(g => g.DifferentialProductId + " " + g.DifferentialDescription).ToArray()) + "]")
				.Take(Math.Min(4, related.Count()))
				.ToArray()));

			Console.WriteLine();

			Console.WriteLine("NavigationChoice currently has " + navigationChoice.PwsObject.ProductListCount + " products.");

			// An demonstrate a search using an anonymous method (i.e. no Token for the Post, which is unusual).
			navigationChoice = application.GetRootService<PCRLPN.NavigationChoice_V1>("navigationchoice");
			navigationChoice = navigationChoice.PostAnonymously<PCRLPN.NavigationChoice_V1>(f => f.Searches, new PCRLPN.NavigationChoice_V1()
			{
				RequestedSearch = new PCRLPN.NavigationChoice_V1.SearchTerms()
				{
					Keywords = new List<string>() { productList.First().PwsObject.ProductId } // Dummy search - look for a given product ID that's known to be in the current NavigationChoice.
				}
			});
			Console.WriteLine("Result of searching for a product like " + productList.First().PwsObject.ProductId + " within current NavigationChoice position: NavigationChoice with " + navigationChoice.PwsObject.ProductListCount + " product.");

			// And to remove the search, post an empty search back again.
			navigationChoice = navigationChoice.PostAnonymously<PCRLPN.NavigationChoice_V1>(f => f.Searches, new PCRLPN.NavigationChoice_V1());
			Console.WriteLine("Result of removing search filter: NavigationChoice with " + navigationChoice.PwsObject.ProductListCount + " products" + Environment.NewLine);

			Console.WriteLine();
		}


		/// <summary>
		/// Based upon the catalogue information, retrieve a full set of net prices for a customer.
		/// </summary>
		private static void DemonstrateNetPricingRetrieval()
		{
			IPwsApplication application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// NavigationChoice is the basis for obtaining net prices. Products returned are in the context of the NavigationChoice, so choosing
			// the root NavigationChoice will return all products.
			var allPrices = application.GetDefaultToken()
				.Follow<PCRLC.Customer_V1>(f => f.PrimaryCustomer)
				.Follow<PCRLPN.NavigationChoice_V1>(f => f.ProductNavigation)
				.FollowList<PCRLP.ProductInformation_CustomerProduct_V1V1>(f => f.ProductList);

			// Some clients are permitted to see pricing for many customers. This manifests within the SecondaryCustomers for the token.
			// Get prices for the first 16 customers in the secondary list.
			var allPricesManyCustomers = application.GetDefaultToken()
				.FollowList<PCRLC.Customer_V1>(f => f.SecondaryCustomers)
				.Take(16) // First 16 customers.
				.Select(f => new { Customer = f, Products = f.Follow<PCRLPN.NavigationChoice_V1>(g => g.ProductNavigation).FollowList<PCRLP.ProductInformation_CustomerProduct_V1V1>(g => g.ProductList) }) // Get a list of prices per customer.
				.SelectMany(f => f.Products.Select(g => new { Customer = f.Customer, Product = g })); // Flatten the list into pairs of Customer,Product.

			Stopwatch sw = new Stopwatch();
			sw.Start();
			int count = allPricesManyCustomers.Count();
			sw.Stop();
			Console.WriteLine(count + " prices retrieved for the first 16 secondary customers, taking " + sw.Elapsed.ToString());
		}


		/// <summary>
		/// Suppliers can also use the services. If a supplier is linked to a login, then it is available from the token.
		/// </summary>
		private static void DemonstrateSupplierOperations()
		{
			IPwsApplication application = new PwsApplication(__service, Configuration.EndUserTradezoneUsername, Configuration.EndUserTradezonePassword);

			// Get supplier.
			IPwsObjectWrapper<PCRLSu.Supplier_V1> supplier;
			try
			{
				supplier = application.GetDefaultToken()
					.Follow<PCRLSu.Supplier_V1>(f => f.PrimarySupplier);
			}
			catch (PwsServiceNullRelationshipException ex)
			{
				// No supplier could be linked from this Token.
				// This could be due to the login being associated with a customer but not a supplier, which is quite normal.
				// An alternative approach would be to check for a null PrimarySupplier on the Token, before calling Follow.
				Console.WriteLine(ex.Message);
				return;
			}

			Console.WriteLine("Using Supplier: " + supplier.PwsObject.Name);

			// Get worksurface jobs.
			var worksurfaces = supplier.FollowList<PCRLW.WorksurfaceJob_V1>(f => f.OutstandingWorksurfaceJobs);
			Console.WriteLine("Worksurface jobs outstanding: " + worksurfaces.Count);

			// Focus on the first job.
			if (worksurfaces.Any())
			{
				var worksurface = worksurfaces.First();
#if DEBUG
				// Debug, PWS: test job.
				worksurface = worksurfaces.Where(f => f.PwsObject.WorksurfaceId == "GS777777").FirstOrDefault();
#endif
				Console.WriteLine("Working with: " + worksurface.PwsObject.AccountCode);


				// ============================================================================
				// Stop at this point. Any calls further down are affective live data.
				// Halt in Debug mode, so that the developer has the opportunity to cancel.
				Debug.Fail("Remove this line to allow the example program to continue", "Any operations below this will be editing data, rather than just retrieving it.");

				// Upload a new file to the temporary Uploads area.
				FileInfo file = new FileInfo(@"c:\temp\upload.png");
				if (file.Exists)
				{
					using (FileStream fs = new FileStream(file.FullName, FileMode.Open))
					{
						IPwsObjectWrapper<PCRLE.Users.Upload_V1> upload = application.GetDefaultToken()
							.PostBinaryFile<PCRLE.Users.Upload_V1>(f => f.Uploads, fs, "image/png");

						Console.WriteLine("Uploaded file: " + upload.PwsObject);

						// Attach temporary upload file to the TemplateCompletion object.
						PCRLW.TemplateCompletion_V1 templateCompletion = new PCRLW.TemplateCompletion_V1() { WorksurfaceId = worksurface.PwsObject.WorksurfaceId };
						templateCompletion.PhotoUploadIds.Add(upload.PwsObject.Id);

						// Send TemplateCompletion object to worksurface.
						worksurface.Post<PCRLW.TemplateCompletion_V1>(f => f.TemplateCompletion, templateCompletion);
					}
				}
			}
		}
	}
}
